﻿using Newtonsoft.Json.Linq;
using Niknak.Social.Common.Http;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

namespace Niknak.Social.Twitter.Streams.Public
{
    /// <summary>
    /// Uses the public statuses/filter stream from Twitter
    /// </summary>
    /// <typeparam name="Settings">Concrete type containing settings, i.e. OAuth and factory</typeparam>
    public class Statuses<Settings, Context> : IDisposable 
        where Settings : ISettings, new()
    {
        private const string APIFILTERURL = "https://stream.twitter.com/1.1/statuses/filter.json";
        private Context _ContextObject;

        private ProcessTweet _TweetProcessor;

        /// <summary>
        /// Method to call when each Twitter message comes in from the stream
        /// </summary>
        /// <param name="context">A state/context object that can be passed into the delegate</param>
        /// <param name="status">Indicates the status of the method call</param>
        /// <param name="twitterMessage">Nullable. Instance of specified implementation of IMessage for this Twitter message</param>
        public delegate void ProcessTweet(Context context, Enums.StreamStatus status, IMessage twitterMessage);

        /// <summary>
        /// New instance of the Statuses stream class
        /// </summary>
        /// <param name="tweetProcessor">ProcessTweet delegate called whena message is received from the stream</param>
        /// <param name="context">A state/context object that can be passed into the ProcessTweet delegate</param>
        public Statuses(ProcessTweet tweetProcessor, Context context)
        {
            if (tweetProcessor == null)
                throw new ArgumentNullException("tweetProcessor");

            _TweetProcessor = tweetProcessor;
            _ContextObject = context;
        }

        /// <summary>
        /// Uses the 'track' parameter on the statuses/filter stream for filtering messages by specific keywords (e.g. hashtags or search terms)
        /// </summary>
        /// <param name="keywords">One or more hashtags or search terms to filter on, separated by commas</param>
        public void FilterByKeywords(string keywords)
        {
            Filter("track", keywords);
        }

        /// <summary>
        /// Uses the 'follow' parameter on the statuses/filter stream for filtering messages by specific users
        /// </summary>
        /// <param name="users">One or more user IDs (e.g. 1117122236) to filter on, separated by commas</param>
        public void FilterByUsers(string users)
        {
            foreach (char chr in users)
                if (!new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',' }.Contains(chr))
                    throw new Exception("User ID(s) must be numeric");

            Filter("follow", users);
        }

        /// <summary>
        /// Uses the 'locations' parameter on the statuses/filter stream for filtering messages by location bounding boxes
        /// </summary>
        /// <param name="boundingBoxCoordinates">A comma-separated list of longitude,latitude pairs specifying a set of bounding boxes to 
        /// filter Tweets by. All geolocated Tweets falling within the requested bounding boxes will be included. Each bounding box should 
        /// be specified as a pair of longitude and latitude pairs, with the southwest corner of the bounding box coming first.</param>
        public void FilterByLocations(string boundingBoxCoordinates)
        {
            foreach (char chr in boundingBoxCoordinates)
                if (!new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '.', '-' }.Contains(chr))
                    throw new Exception("Invalid coordinates");

            Filter("locations", boundingBoxCoordinates);
        }

        private void Filter(string param, string paramValue)
        {
            if (_TweetProcessor == null)
                throw new Exception("TweetProcessor delegate not defined");
            if (string.IsNullOrEmpty(param) || string.IsNullOrWhiteSpace(param))
                throw new ArgumentNullException("param");
            if (string.IsNullOrEmpty(paramValue) || string.IsNullOrWhiteSpace(paramValue))
                throw new ArgumentNullException("paramValue");

            ServicePointManager.Expect100Continue = true;
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            using (HttpClient client = new HttpClient(new OAuthMessageHandler<Settings>(new HttpClientHandler())))
            {
                if (client == null)
                    throw new Exception("Unable to initiate HttpClient object");

                client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);

                if (paramValue.Contains(','))
                    paramValue = string.Join("%2C", paramValue.Split(',').Select(kw => HttpUtility.UrlEncode(kw)).ToArray());
                else if (param != "locations")
                    paramValue = HttpUtility.UrlEncode(paramValue);

                ISettings settings = new Settings();
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, APIFILTERURL + "?" + param + "=" + paramValue);

                Task postFilter = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
                    .ContinueWith(postTask =>
                    {
                        HttpResponseMessage response = postTask.Result;

                        if (!response.IsSuccessStatusCode)
                            throw new Exception("Bad response status code: " + response.ReasonPhrase);

                        Task streamReader = response.Content.ReadAsStreamAsync().ContinueWith(readerTask =>
                        {
                            string streamLine = null;

                            using (StreamReader reader = new StreamReader(readerTask.Result))
                            {
                                while (!_StopStreaming && !reader.EndOfStream)
                                {
                                    streamLine = reader.ReadLine();

                                    if (string.IsNullOrEmpty(streamLine) || streamLine.Length < 5)
                                        Debug.WriteLine("Received keep-alive message from Twitter stream");
                                    else
                                        {
                                            JObject tweetJson = null;
                                            IMessage parsedTweet = settings.Factory.NewMessage();
                                            parsedTweet.Text = "Not initialised";

                                            Enums.StreamStatus status = Enums.StreamStatus.OK;

                                            try
                                            {
                                                tweetJson = JObject.Parse(streamLine);

                                                //check for rate limit message
                                                if (tweetJson.Property("limit") != null)
                                                {
                                                    status = Enums.StreamStatus.RateLimit;
                                                    parsedTweet = settings.Factory.NewMessage();
                                                    parsedTweet.Text = streamLine;
                                                }

                                                //check for disconnect - e.g. {"disconnect":{"code":7,"stream_name":"SimonHoade-statuses70209","reason":"admin logout"}}
                                                if (tweetJson.Property("disconnect") != null)
                                                {
                                                    status = Enums.StreamStatus.Disconnected;
                                                    parsedTweet = settings.Factory.NewMessage();
                                                    parsedTweet.Text = streamLine;
                                            }

                                                if (status == Enums.StreamStatus.OK)
                                                    parsedTweet = Utils.ParseTweetJson<Settings>(tweetJson);
                                            }
                                            catch (Exception ex)
                                            {
                                                status = Enums.StreamStatus.BadMessage;
                                                parsedTweet = settings.Factory.NewMessage();
                                                parsedTweet.Text = ex.Message + "; " + (tweetJson != null ? tweetJson.ToString() : "Couldn't parse JSON");
                                        }

                                            try
                                            {
                                                _TweetProcessor(_ContextObject, status, parsedTweet);
                                            }
                                            catch (Exception ex)
                                            {
                                                Debug.WriteLine("Caught unhandled exception in _TweetProcessor delegate; " + ex.Message);
                                            }
                                        }
                                }
                            }
                        });
                    });

                Task.WaitAll(postFilter);
            }
        }

        private bool _StopStreaming = false;

        /// <summary>
        /// Disconnects from the Twitter stream
        /// </summary>
        public void StopStreaming()
        {
            _StopStreaming = true;
        }

        /// <summary>
        /// Required for IDisposable. Calls StopStreaming.
        /// </summary>
        public void Dispose()
        {
            StopStreaming();
        }
    }
}
